Skip to content

Story 2296: V3 Post Detail page#2373

Open
julioest wants to merge 28 commits into
boostorg:developfrom
julioest:feat/v3-post-detail-2296
Open

Story 2296: V3 Post Detail page#2373
julioest wants to merge 28 commits into
boostorg:developfrom
julioest:feat/v3-post-detail-2296

Conversation

@julioest
Copy link
Copy Markdown
Collaborator

@julioest julioest commented Apr 29, 2026

Summary & Context

Implements Story 2296: Webpage UI: Posts Detail. Adds the V3 post detail page, mounted on the polymorphic Entry model so blog posts, news, links, videos, and polls all render through the same layout. Built on the V3 registry pattern from #2308 (V3Mixin). Reuses existing V3 design-system includes (_user_profile, _post_card -> _card_group) and adds one new reusable include (_post_header).

Figma link:

Link to components/page:

Changes

Routing

  • The V3 detail page is served at the existing /news/entry/<slug:slug>/ URL. No new route. EntryDetailView picks between the legacy and V3 templates based on the v3 waffle flag.

View (news/views.py)

  • EntryDetailView is now V3Mixin + DetailView, with template_name = "news/detail.html" and v3_template_name = "news/v3/detail.html". Returns the legacy template by default and the V3 template when the flag is on. Verified by core.tests.test_v3_registry.
  • Permission gating via Entry.can_view is unchanged.
  • Real DB queries for next + related (no mock data):
    • Next: oldest published Entry whose publish_at is after the current's; excludes self.
    • Related: 3 most recent published entries; excludes self and the next post.
    • Both filter deleted_at__isnull=True and use pk as a secondary sort key for stable ordering when publish_at ties.
    • All querysets use select_related("author") and prefetch_related("author__badges", "author__maintainers") to avoid N+1.
  • EntryModerationDetailView opts out of V3 rendering with v3_template_name = None; iter_v3_views() still surfaces it for the V3 Demo registry, while the drift test filters out opted-out views before checking templates.
  • _post_card_item builds the dict shape _post_card.html expects.
  • _post_card_item exposes entry.tag under the category key (capitalized via TAG_LABELS), since the tag key on _post_card.html is reserved for library hashtags like #beast that aren't yet in the model.
  • Author profile card is built by users.profile_cards.user_profile_card, a shared helper that other components (testimonial card, post card, user card) can reuse.
  • TAG_LABELS = {"blogpost": "blog"} so the meta tag renders a friendlier label.
  • Author role is derived from user.maintainers (reverse of LibraryVersion.maintainers): "Maintainer" if the user maintains any library version, otherwise "Contributor".
  • Author badge is the user's first Badge, mapped via the static-image convention static/img/v3/badges/badge-{name}.png. Renders nothing when the user has no badges.

Admin chrome on the V3 detail

  • Admin actions row above the post header: Approve form (when unapproved and not deleted), "Pending Moderation" badge (for non-approvers), Edit, and Delete buttons. Uses the v3 button component with green / secondary / error styles.
  • Admin-only "entry deleted" notice at the top of the article when an entry is soft-deleted, mirroring the legacy template. Non-admins still 404 via can_view, so the notice only renders for users authorized to view deleted entries.

Body rendering

  • Body renders through wagtail-markdown's |markdown filter chained with urlize for raw URLs. Real <ol> / <ul> lists, autolinked URLs, and bold/italic/code/headings render naturally for new content authored in the WYSIWYG. Aligns with the post creation flow's markdown output.
  • Body prefers object.content; falls back to object.visible_content (AI summary) for Link entries with no content of their own.
  • object.external_url surfaces as a link for Link entries.
  • Author email is no longer used as a fallback for missing display name.

New reusable include (templates/v3/includes/_post_header.html)

  • Inputs: title, publish_date, optional tag, optional author.
  • Internally uses _user_profile.html for the author block.

CSS

  • New static/css/v3/post-header.css and static/css/v3/post-detail.css.
  • Card-group and post-card refinement: page-flush borders, outer-corner rounding only, transparent card-group background in light and dark, subtle per-card background.
  • Scoped styles in .post-detail__body for lists, headings (h1–h6), bold, and code/pre so markdown output picks up the v3 type system.

‼️ Risks & Considerations ‼️

Body rendering is plain text + autolinking, not markdown. Ticket criterion 4 calls for markdown with Boostlook 2.0 styling. Boostlook 2.0 isn't ready, so this PR leaves markdown for a follow-up. Bold/italic/code-fence syntax in source will appear literally until then. Body now renders markdown. I'm leaving boostlook out of this flow: it's a heavy framework geared toward documentation templates, and the scoped v3 CSS in post-detail.css already handles the markdown formatting we need (paragraphs, lists, headings, code).

Entry.objects.published() covers all subtypes. Next/Related can surface BlogPost, Link, News, Video, or Poll entries. If the editorial intent is "blog detail only shows blog-style siblings," that's a follow-up filter.

Role rule is a chosen default. "Maintains at least one LibraryVersion -> Maintainer" is a reasonable starting rule; if the team wants something stricter (current versions only, primary library, etc.) the predicate is one line in user_profile_card.

Badges depend on data. The Badge model has only name/display_name (no image field). The helper assembles static/img/v3/badges/badge-{name}.png from Badge.name based on the existing static-asset convention used in mock data. Wiring is live; nothing renders until badges are seeded and assigned.

Description on Next/Related cards is plumbed but not yet rendered. _post_card_item exposes entry.summary as description on each card dict, but _post_card.html (the in-flight PostFeed work) doesn't read the field yet. Once that include lands, the cards will start showing the AI summary automatically with no further changes on this side.

The next/related card date format needs to be updated in _post_card.html. The post-header now uses the written-out format (e.g. "November 11th, 2024"), but the cards still render d/m/Y because the format lives in the shared _post_card.html include.

The admin actions UI needs a design decision. The Approve, Pending Moderation, Edit, and Delete controls don't have a Figma spec in V3, so the layout (left-aligned row above the post header) and styling (v3 button component with green / secondary / error) are developer judgment. Worth a design review before this ships broadly.

Screenshots

2296-post-detail-desktop 2296-post-detail-mobile
@henryajisegiri we're going to need design for these buttons image

Self-review Checklist

  • Tag at least one team member from each team to review this PR
  • Link this PR to the related GitHub Project ticket

Frontend

  • UI implementation matches Figma design
  • Tested in light and dark mode
  • Responsive / mobile verified
  • Accessibility checked (keyboard navigation, etc.)
  • Ensure design tokens are used for colors, spacing, typography, etc. – No hardcoded values
  • Test without JavaScript (if applicable)
  • No console errors or warnings

@julioest julioest force-pushed the feat/v3-post-detail-2296 branch from a8e4ca1 to 08602ac Compare April 29, 2026 15:53
@julioest julioest marked this pull request as ready for review April 29, 2026 17:23
@julioest julioest force-pushed the feat/v3-post-detail-2296 branch from 08602ac to 60eac1a Compare April 29, 2026 23:35
@herzog0 herzog0 linked an issue Apr 30, 2026 that may be closed by this pull request
Copy link
Copy Markdown
Collaborator

@herzog0 herzog0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All looking pretty good, just a couple of nits and a larger x-padding in mobile that it's smaller in Figma. Pre-approving!

Image

Comment thread static/css/v3/post-detail.css Outdated
Comment thread templates/news/v3/detail.html Outdated
julioest added a commit to julioest/website-v2 that referenced this pull request May 5, 2026
Refs PR boostorg#2373 review feedback. --font-mono is not a defined
design token; the correct identifier for monospace text is
--font-code.
julioest added a commit to julioest/website-v2 that referenced this pull request May 5, 2026
@julioest julioest force-pushed the feat/v3-post-detail-2296 branch from 3fbe4dd to 251defa Compare May 5, 2026 14:28
Copy link
Copy Markdown
Collaborator

@julhoang julhoang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @julioest ! The UI is looking really great! As for the BE, I've left some suggestions for us to consider, and the main change request is for removing the deleted posts from the Next Post/Related Posts queries. 🙏

Comment thread news/templatetags/news_tags.py Outdated
Comment on lines +12 to +27
@register.filter
def text_paragraphs(value):
"""Render hard-wrapped plain text as autolinked paragraphs.

Blank lines become paragraph breaks; single newlines inside a
paragraph collapse to spaces so source hard-wrapped at ~80 chars
flows naturally to the container width.
"""
if not value:
return ""
paragraphs = []
for chunk in _PARAGRAPH_SPLIT.split(str(value)):
text = " ".join(line.strip() for line in chunk.splitlines() if line.strip())
if text:
paragraphs.append(f"<p>{urlize(text, autoescape=True)}</p>")
return mark_safe("\n".join(paragraphs))
Copy link
Copy Markdown
Collaborator

@julhoang julhoang May 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I think forcing single newlines into spaces might break the formatting of some lists in the old posts, please see this example:

Image

(Notice the numbered list and * list, the single newline on Thank you, was intentional too)

Is it possible to refine the strategy here a bit more to not affect lists? 🤔

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I refined this

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, why do we need this at all @julioest? Shouldn't this area just render regular markdown, without the need for any manual customization?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@julioest I'm unresolving this thread for better visibility since @herzog0 posted a question above, could you help us add a response to that first? 😊🙏

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, y'all are both right. Stripped the manual stuff and switched the body to wagtail-markdown (f7b8b60). Now, the lists, autolinked URLs, and bold/italic/code now render naturally.

Heads up: legacy Thank you,\nName sign-offs collapse to one line since markdown treats single newlines as soft breaks; authors can drop a blank line if they want the break back. Tested this and it renders this -

image

Comment thread news/views.py Outdated
Comment thread news/views.py Outdated
Comment thread news/views.py Outdated
Comment thread news/views.py Outdated
Comment thread news/views.py Outdated
julioest added 16 commits May 7, 2026 09:04
Adds the V3 post detail page at /v3/news/entry/<slug>/, used
by all Entry types (blog, news, link, video, poll).

- New V3PostDetailView with real next/related queries.
- Reusable _post_header include (title, meta, author block).
- Body escapes user content and falls back to summary when
  content is empty.
Source content hard-wrapped at ~80 chars was rendering a
forced <br> on every soft newline. The new filter splits on
blank lines into <p> tags and joins single newlines with
spaces, so prose flows to the container width. Autolinks
URLs and escapes HTML; preserves XSS protection.
- Body typography per Figma: text-secondary, line-height
  135%, letter-spacing, paragraph spacing via flex gap.
- 1px separator line above body with 32px on each side.
- 64px gap between body and first sibling section; 32px
  between Next Post and Related Post.
- Card-group + post-card refinement: page-flush borders,
  outer-corner rounding only, transparent card-group
  background in light and dark, subtle per-card background.
- Post-header bullet now a CSS-drawn dot.
- Spacing fix: Figma "xxl" (32px) maps to --space-xl.
Author role derives from user.maintainers (Maintainer when the
user maintains any LibraryVersion, Contributor otherwise).
Badge picks the user's first Badge and points at the static
convention static/img/v3/badges/badge-{name}.png. Both lookups
are batched via prefetch_related on the entry, next, and
related querysets.
_post_card_item now exposes entry.summary as description so
the cards have content for the description slot the PostFeed
work in flight will render. No template changes here; the dict
is plumbed end-to-end on our side and waits for the PostFeed
include to read it.
Link entries render their external URL as the link text, which
screen readers spell out character by character. The new
aria-label gives a meaningful announcement (post title plus
new-tab cue) while keeping the URL visible for sighted users.
Focus-visible styling is already covered globally by
v3-style-overrides.css; body text contrast meets WCAG AA in
both light and dark modes.
Mobile (<768px):
- post-header gap raised to --space-large.
- 64px between article body and the first sibling section.
- 64px page padding-bottom (gap below the last related card).

Tablet (768-1279px):
- post-header gap raised to --space-large.
The post-header date now reads m/d/Y per design. Next/related
card dates still render d/m/Y since that format lives in the
shared _post_card.html include, which is also used by the
homepage, learn page, and component demo. Updating those is a
separate change.
Entry.objects.published() filters published=True but not the
soft-delete flag, so deleted posts could surface in the V3
post detail Next Post and Related Posts sections. Filter
deleted_at__isnull=True on both querysets.
Add pk as a secondary sort key on the V3 next-post and
related-posts queries so results stay deterministic when
multiple entries share the same publish_at.

Restore the admin-only "entry deleted" notice at the top
of the V3 detail article, mirroring the legacy template.
Non-admins still 404 via can_view, so the notice only
renders for users authorized to view deleted entries.
Move the dict builder for v3/includes/_user_profile.html
out of V3PostDetailView into users/profile_cards.py so
other views (testimonial card, post card, user card) can
reuse it without duplicating the maintainer/badge logic.
text_paragraphs was collapsing every single newline to a
space, which destroyed numbered and bullet lists in legacy
posts and merged intentional line breaks like sign-offs
into the surrounding sentence.

Detect author-formatted paragraphs (any list marker, or
any non-final line under 60 chars) and preserve their
breaks as <br>. Genuine hard-wrapped prose still collapses
to flow at the container width.
@julioest julioest force-pushed the feat/v3-post-detail-2296 branch from 9b4ec4f to 2b01c95 Compare May 7, 2026 13:06
Folds V3PostDetailView into EntryDetailView via V3Mixin so
the same /news/entry/<slug>/ URL serves either template based
on the v3 waffle flag, dropping the temporary /v3/news/entry/
route. Per julhoang's review on PR boostorg#2373.

Ports the admin actions row (Approve, Edit, Delete) and
Pending Moderation badge into the V3 detail template so
moderators keep those controls when the v3 flag is active.

EntryModerationDetailView opts out of v3 rendering with
v3_template_name = None; iter_v3_views skips views without
a v3 template so the registry test ignores the opt-out.
@julioest julioest requested a review from julhoang May 7, 2026 13:36
Copy link
Copy Markdown
Collaborator

@julhoang julhoang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @julioest ! I have a couple of additional small requests below. 🙏

Also, I've just realized that while the Create Post is generating markdown (via WYSIWYG), this Post Detail page seems to be expecting rich text – we should agree upon 1 format for this flow. 🤔

Comment thread core/mixins.py Outdated
Comment thread news/templatetags/news_tags.py Outdated
Comment on lines +12 to +27
@register.filter
def text_paragraphs(value):
"""Render hard-wrapped plain text as autolinked paragraphs.

Blank lines become paragraph breaks; single newlines inside a
paragraph collapse to spaces so source hard-wrapped at ~80 chars
flows naturally to the container width.
"""
if not value:
return ""
paragraphs = []
for chunk in _PARAGRAPH_SPLIT.split(str(value)):
text = " ".join(line.strip() for line in chunk.splitlines() if line.strip())
if text:
paragraphs.append(f"<p>{urlize(text, autoescape=True)}</p>")
return mark_safe("\n".join(paragraphs))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@julioest I'm unresolving this thread for better visibility since @herzog0 posted a question above, could you help us add a response to that first? 😊🙏

Comment thread templates/v3/includes/_post_header.html Outdated
Comment thread templates/v3/includes/_post_header.html Outdated
Comment thread static/css/v3/post-header.css Outdated
Comment thread news/views.py Outdated
julioest added 6 commits May 10, 2026 01:45
Render the publish date as "May 8th, 2026" (F jS, Y) per
review feedback, and drop the orphan blank line between
the tag span and {% endif %} in _post_header.html.
Per review feedback, swap the post-header title tracking
from --letter-spacing-display-regular (-0.02em) to
--letter-spacing-tight (-0.01em).
Per review feedback, expose entry.tag under the "category"
key in _post_card_item rather than "tag", since the
_post_card component uses "tag" for library hashtags
(e.g. #beast) which we don't store yet.

Also pass the value through TAG_LABELS so next/related
cards display "blog" for blogposts, matching the post
header.
Move the "has v3 template" filter out of iter_v3_views and
into the test that needs it. The discovery function now
returns every V3Mixin subclass, including ones that opt
out of v3 rendering with v3_template_name = None (like
EntryModerationDetailView). The V3 Demo registry can show
them again, while the drift test still only verifies real
templates load.
Swap text_paragraphs for the standard markdown filter
(via wagtail-markdown) plus urlize, so post bodies
align with what Create Post outputs from the WYSIWYG
and where Wagtail RichTextField is heading. Real <ol>
and <ul> lists, autolinked URLs, and bold/italic/code
all render for new posts. Add scoped styles for lists,
headings, and strong inside .post-detail__body so the
rendered markdown picks up the v3 type system.

Heads up: legacy "Thank you,\nName" sign-offs now
collapse to one line since markdown treats single
newlines as soft breaks. Authors can drop a blank
line if they want the break back.
Most of .post-detail__body p, the li block, and the margin
on h1-h6 were just restating things that already inherit or
that the global stylesheet resets. Slim them down, move the
shared letter-spacing up to .post-detail__body, and leave a
note on the p padding override since frontend/styles.css
applies py-5 to every <p> and that would otherwise break
the flex gap rhythm here.
@julioest julioest requested review from herzog0 and julhoang May 10, 2026 07:47
Match the post header's date format (e.g. "January 30th,
2026") on next/related cards, the homepage community feed,
and the community page. One-line change in the shared
_post_card.html include so all three surfaces stay in sync.
Copy link
Copy Markdown
Collaborator

@julhoang julhoang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for all the updates @julioest , the markdown rendering looks great! I'm pre-approving with 2 tiny nits 🙏

Comment thread news/views.py Outdated
Comment thread templates/v3/includes/_post_card.html Outdated
julioest added 2 commits May 11, 2026 22:23
Aligns the constant name with the card's `category` key
(and the post header's "Blog" / "News" / "Link" display
label) instead of the source field name `entry.tag`.
Per design: cards switch from "January 30th, 2026" to
"Jan 30th, 2026". One-character format change (F -> M).
The post header keeps the full month name since it has
more room.
Copy link
Copy Markdown
Collaborator

@herzog0 herzog0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heya sorry for my initial oversight. Just a comment on the next_entry query!

Comment thread news/views.py
Comment on lines +252 to +260
next_entry = (
Entry.objects.published()
.select_related("author")
.prefetch_related(*self.AUTHOR_PREFETCH)
.filter(publish_at__gt=entry.publish_at, deleted_at__isnull=True)
.exclude(pk=entry.pk)
.order_by("publish_at", "pk")
.first()
)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I think the next post should actually be the one relative to the current one, otherwise it'll always show the most recent post for all posts

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I dug into this a bit so I can better understand. I ran the same queryset in the dev shell with full timestamps. The query does return the chronologically next post, not the most recent overall.

2023-10-10 10:12:22 "The Boost.Asio property system" -> "CppCon YouTube Channel 100k Subscriber Milestone!"
2023-10-12 14:54:38 "CppCon YouTube Channel 100k Subscriber Milestone!" -> "CppCon 2023 Trip Report"
2023-10-19 17:11:06 "CppCon 2023 Trip Report" -> "Sam's Q3 Projects"
2023-10-25 17:13:24 "Sam's Q3 Projects" -> "Joaquín's Boost.Unordered Update"
2023-10-27 16:50:50 "Joaquín's Boost.Unordered Update" -> "Alan's Work on MrDocs and Handlebars"
2023-10-27 17:16:38 "Alan's Work on MrDocs and Handlebars" -> "Christian's Unordered Update"
2023-10-27 17:17:18 "Christian's Unordered Update" -> "Fernando's Adventures in Boost"
2023-10-27 17:29:32 "Fernando's Adventures in Boost" -> "Klemens Boost.Async"
2023-10-27 17:30:32 "Klemens Boost.Async" -> "Peter Turcan Documentation Status"
2023-10-28 17:20:46 "Peter Turcan Documentation Status" -> "Matt's Charconv and Decimal Update"

The 2023-10-27 cluster makes it clear: five posts within ~40 minutes, each pointing at the next neighbor by timestamp.

This direction matches v2's get_next_by_publish_at, so this feels more like a design call than a bug. Happy to flip it either way though.

Let us know whatcha think @henryajisegiri

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @julioest we can leave this for the integration ticket then. Thanks a ton for the digging. Henry is also aware we're leaving this for later.

Comment thread templates/news/v3/detail.html Outdated
Comment thread news/views.py
julioest added 2 commits May 12, 2026 23:24
It's below the fold for most viewports, so deferring the fetch cuts
initial-paint bytes without affecting readers who scroll.
related_qs was meant to filter by libraries linked to the current
entry, not "any other published post." That relation doesn't exist
in the schema yet, so a TODO captures the intent until it does.
@julioest julioest requested a review from herzog0 May 13, 2026 04:27
Copy link
Copy Markdown
Collaborator

@herzog0 herzog0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing the issues!

@julioest julioest requested a review from kattyode May 13, 2026 21:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Webpage UI: Posts Detail

4 participants